package se.lnu.axez.shaders;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

import android.content.res.Resources;

public class ShaderManager {
	private final ShaderFactory shaderFactory;
	private final Map<Class<? extends VertexShader>, VertexShader> vertexShaders = new HashMap<Class<? extends VertexShader>, VertexShader>();
	private final Map<Class<? extends FragmentShader>, FragmentShader> fragmentShaders = new HashMap<Class<? extends FragmentShader>, FragmentShader>();
	private final Map<ProgramKey, ShaderProgram> programs = new HashMap<ProgramKey, ShaderProgram>();

	public ShaderManager(ShaderFactory shaderFactory) {
		if (shaderFactory == null) {
			throw new IllegalArgumentException("shaderFactory cannot be null");
		}
		
		this.shaderFactory = shaderFactory;
	}
	
	public <E extends VertexShader> E getVertexShader(Class<E> type) {
		if (this.vertexShaders.containsKey(type)) {
			return (E) this.vertexShaders.get(type);
		}

		try {
			E instance = this.shaderFactory.createNewInstance(type);
			this.vertexShaders.put(type, instance);
			return instance;
		} catch (Exception e) {
			// reconsider this
			throw new RuntimeException();
		}
	}
	
	public <E extends FragmentShader> E getFragmentShader(Class<E> type) {
		if (this.fragmentShaders.containsKey(type)) {
			return (E) this.fragmentShaders.get(type);
		}

		try {
			E instance = this.shaderFactory.createNewInstance(type);
			this.fragmentShaders.put(type, instance);
			return instance;
		} catch (Exception e) {
			// reconsider this
			throw new RuntimeException();
		}
	}
	
	public ShaderManager loadShaders() {
		for (VertexShader vs : this.vertexShaders.values()) {
			vs.load();
		}
		
		for (FragmentShader vs : this.fragmentShaders.values()) {
			vs.load();
		}
		
		for (ShaderProgram sp : this.programs.values()) {
			sp.load();
		}
		
		return this;
	}
	
	public ShaderProgram getShaderProgram(VertexShader vertexShader, FragmentShader fragmentShader) {
		ProgramKey key = new ProgramKey(vertexShader.getShaderID(), fragmentShader.getShaderID());
		if (this.programs.containsKey(key)) {
			return this.programs.get(key);
		}
		
		ShaderProgram program = new ShaderProgram(vertexShader, fragmentShader);
		this.programs.put(key, program);
		return program;
	}
	
	public ShaderManager disposeAll() {
		// no need to dispose shader data - it is removed when OpenGL context becomes invalid
		this.programs.clear();
		this.vertexShaders.clear();
		this.fragmentShaders.clear();
		return this;
	}
	
	public static final class ShaderFactory {
		private Resources resources;
		
		public ShaderFactory(Resources resources) {
			if (resources == null) {
				throw new IllegalArgumentException("resources cannot be null");
			}
			
			this.resources = resources;
		}
		
		private <E> E createNewInstance(Class<E> type)
				throws IllegalArgumentException, InstantiationException,
				IllegalAccessException, InvocationTargetException,
				SecurityException, NoSuchMethodException {
			Constructor<E> ctor = type.getConstructor(Resources.class);
			if (ctor != null) {
				return ctor.newInstance(this.resources);
			} else {
				return type.newInstance();
			}
		}
	}
	
	private final class ProgramKey {
		private final int vertexShaderID;
		private final int fragmentShaderID;
		
		ProgramKey(int vertexShaderID, int fragmentShaderID) {
			this.vertexShaderID = vertexShaderID;
			this.fragmentShaderID = fragmentShaderID;
		}
		
		@Override
		public int hashCode() {
			int hash = 7;
			hash = 31 * hash + this.vertexShaderID;
			hash = 31 * hash + this.fragmentShaderID;
			return super.hashCode();
		}
		
		@Override
		public boolean equals(Object obj) {
			if (this == obj) {
				return true;
			}
			
			if((obj == null) || (obj.getClass() != this.getClass())) {
				return false;
			}
			
			ProgramKey other = (ProgramKey)obj;
			return (this.vertexShaderID == other.vertexShaderID
					&& this.fragmentShaderID == other.fragmentShaderID);
		}
	}
}
